一路上感謝各位讀者們的支持和回饋。
本 30 天系列文目前已經將篇幅重新整理、編纂成冊。
《JavaScript 概念三明治》在天瓏書局上架囉!
喜歡這個系列,想閱讀更詳細原理說明的讀者可以參考:
https://www.tenlong.com.tw/products/9789864347575
上一篇針對 Promise 的語法做了一個基本的解說,但其實今天的內容才是我想講的,Promise 的運作邏輯不難理解,但若是 Promise 在整個 JS 以及瀏覽器裡的流程可能就比較複雜了,現在我們都知道幾件事情:
而雖然在上一章節一直提到非同步,但是對於 Promise 裡所謂非同步執行的部分,目前我們還是沒有很明確的解釋,到底是哪一部分會以非同步的方式被執行?以及什麼時候會執行?這是這篇文章想要探討跟說明的。
我們在 Event Queue 章節裡面所提到 Web API 有些具有非同步的行為,而在非同步的目的達成之後,瀏覽器會把給定的對應的函式推送到 Event Queue 裡面,這些一個一個函式正好代表每一件要做的事情,因此在 JS 裡面,以「 Task 」或 「Macrotask 」來稱呼,為了避免混淆,以下將用 Macrotask Queue 來指稱之前提到的 Event Queue 。
關於 Task 有兩個細節可以注意:
Microtask 通常由 Promise 產生,Promise 裡用到的 .then / .catch 函式會以非同步的方式來被執行,回想下 Queue 的概念,所以的非同步行為指的是,會在全域執行環境執行完之後才被執行,因此一但 Promise 的 callback 內容執行完成,狀態再也不是 pending 時,.then 或 .catch 的函式內容就會被推送到 Queue 裡面等待執行,這個被推送到 Queue 的函式就是 Microtask。
相對於管理 Web API 所屬事件的 Macrotask Queue ,Promise 產生的 Microtask 也有自己的 Queue ,在 JS 內被稱為 Job Queue 或 Microtask Queue,而 Job Queue 與 Event Queue 運作方式上有一點不一樣。
差在哪裡呢?在 Queue 裡面的每個 Macrotask 執行完畢後 ,就算 Event Queue 裡面還有其他的 Task,JS 引擎依舊會優先執行 Microtask Queue 裡面的所有 Task ,在這個同時也不會重新渲染網頁,換句話說,Microtask 的執行是穿插在每個 Macrotask 之間,兩者的差異也就在執行順序的不同而已。
如果還是覺得很抽象,下面我會帶個例子,直接用程式碼來比較 Macrotask 與 Microtask 執行順序的不同,應該比較能夠讓你了解,看看下面的程式碼:
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("promise"));
alert("global ex. context");
這段程式碼剛好同時用到 Web API 與 Promise ,各自在呼叫後會產生一個 Macrotask 以及 Microtask ,不過在順序上是哪個會先被執行呢?稍微分析一下:
因此推測 alert 的順序應該會像是這樣:
"global ex. contenxt"
"timeout"
"promise"
但是並不是!結果會是 "promise"
比 "timeout"
還要更先被 log 出來:
"global ex. contenxt"
"promise"
"timeout"
這是為什麼呢?這邊可能會有點抽象,前面我們在分析 JS 語法與運作模式的時候,大多是從 JS 引擎的角度出發。而前面也有提到, Queue 的概念並不屬於 JS 引擎的一部分,相對的歸屬於瀏覽器。對於瀏覽器來說,在網頁頁面開啟時,載入對應的 JS 檔並且執行這件事情,也是一個 Macrotask 。
而剛剛提到 Macrotask 執行完畢後,會優先執行 Microtask ,因此你會看到 "promise"
出現的順序先於 "timeout"
。
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("promise"));
alert("global ex. context");
這個執行的結果有筆誤打錯嗎?
Hi, Nono :
很抱歉是我筆誤了,這邊是想表達在渲染完之後會先執行 MicroTask ,所以 Promise 的內容會先被執行!非常感謝你的回復,看來我整個系列寫完之後要再仔細校稿一次...
最近也剛好在研究 event loop 相關的資料。
普遍找到的資料都是說再一次 event loop 循環中
2.
到步驟 3.
execution context stack
應該要是空的,才執行 microtask3.
執行到 microtask queue 為空為止依照上面的模型來跑
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("promise"));
alert("global ex. context");
setTimeout
,因為發現是 delay time 為 0,所以把他的 callback 直接放入 task queue 中Promise.resolve()
會產生 microtask,放入 microtask queue 中execution context stack
也為空,執行 promise
的內容setTimeout 的 callback
感覺和作者畫的最後那張圖不太一樣
以上是個人最近研究的一些心得,有任何錯誤請見諒
附上一些參考資料
asks, microtasks, queues and schedules
whatwg - Event loop processing model
Hi , Nono :
基本上我的理解跟你寫的五步驟是差不多的,所以只要了解幾個重點:
At the execution of any JS file, the JS engine wraps the contents in a function and associates the function with an event either start or launch. The JS engine emits the start event, the events are added to the task queue (as a macrotask).
而為什麼你會覺得有問題的差別大概就在第三點吧,因為我想強調這點,才把它放進堆疊圖內,但是可能 Event Loop 的形狀太像是迴圈了,可能因此容易誤導執行順序,不知道你是不是因為這樣才覺得疑惑,如果是的話,我到時候可能要回頭重新想一下怎麼呈現這裡的概念模型,先感謝你的討論!
Hi, Mooji:
非常感謝你的回覆!
那這樣我覺得應該是圖片上的理解有點會讓人誤會
附上之前找到的一張還不錯的圖給你做參考
event loop
你提供的這篇文章滿有趣的還自己用 js 做範例!
最後能偷偷問一下你的圖怎麼做的嗎XD
感謝你百忙之中回覆我
鐵人賽辛苦啦!剩沒幾天了~加油!
Hi Nono:
抱歉剛剛發現之前好像忘記回你XD
我那個時候是用 Sketch 來畫的,
雖來他原來的功能是用來做UI介面的,
但是我發現也很適合用來畫概念圖,就拿來試試看了,
看來成效還不錯!
Hi Nono,請問4. 此 task 執行完畢是指什麼task? 模型 step2的執行 macrotask好像底下沒有跑到?
Hi 您好
最近我在研究Event Loop 的機制
但有一點不太明白,
很多文件說明在Event Loop的第一步是:
"從macrotask queue裡面取出oldest task放到event loop內執行"
然後要在這個task執行完後 才會將所有的microtask 執行完畢
最後再將其他macrotask執行完畢
但是我目前測試的結果 都會是先印出microtask的結果 再印出剩下的macrotask的結果
例如這個測試 https://jsfiddle.net/9k153st8/
這樣跟上面的流程好像有點不太一樣
不知道是哪個地方我有認知錯誤
還是哪個流程有誤
能否麻煩您在幫我指正一下
謝謝
嗨ceall8650:
你這個問題我也有疑惑過,
後來我找到的一個比較普遍的解釋方式是,
對瀏覽器內的 Event Loop 來說,第一次載入 JS 來執行這個動作,也是一個 MacroTask ,所以在全域環境的所有堆疊結束之後,它會直接執行 MicoTask 裡面的 Task。
可以參考:
https://stackoverflow.com/questions/52019729/why-is-this-microtask-executed-before-macrotask-in-event-loop
或是範例比較複雜的這篇:
https://www.linkedin.com/pulse/javascript-under-hood-microtasks-macrotasks-eliran-elnasi/?trk=read_related_article-card_title
不知道這樣有沒有解答到你的問題?有什麼新的結論也可以再跟我說!
Mooji 感謝您的回覆與分享
我有再看了一下, 應該是原本的JS也算是external script 的一種沒錯, 所以算是macrotask.
而之前搞混的原因主要是認為 "microtask會比macrotask執行" 的原因, 所以在event loop 時, macrotask應該是最後一個步驟. 但我認為應該是microtask 是最後一個步驟, 如果microtask執行完後 macrotask queue仍有task, 應該是開始下一個loop. 也就符合Event Loop 執行的順序
以上是我的看法, 看這樣理解你覺得是否合理
你說的最後一個步驟是什麼意思?應該沒有所謂真正的最後一個步驟,只有誰會比誰會先被執行的問題,基本上搞清楚這個順序應該就可以解答你的問題
恩...我這邊的步驟指的是每一次Loop的時候, 會做的哪些步驟.
我是這邊是參考這兩個的地方來幫助我理解
https://javascript.info/event-loop#summary
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
我覺得在定義"loop"的時候應該也會有它的意義, 所以我是認為應該會有"第一步"跟"最後一步", 然後再重新回到第一步, 形成一個loop. 或許這樣比較符合這樣的意思.
以上是我自己的解讀跟看法
感謝您撥空回覆